package com.stottlerhenke.simbionic.util;

import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
 * Takes a Java source or class file and generates a SimBionic class specification
 * 
 * There are two class spec generators. One of them is developed by Pan Rong that uses javadoc to generate class spec from java source files (I will call it javadoc version).
 * The other is from an existing Simbionic class that generate the class spec from a list of java classes (I will call it SB version). 
 * We will make use of both of them to process the following:
 *	1. java source fullpath filename (method generateClassSpecFromSource, javadoc version) 
 *	2. java source path and java package name (method generateClassSpecFromPackageSource, javadoc version)
 *	Note that the package can contain other packages
 *	3. filename of a file containing a list of java class names, and the (optional) classpath. (method generateClassSpecFromClass, SB version) 
 *	4. jar name (extract all the classes from the jar file and process them (method generateClassSpecFromJar, SB version)
 *  5. java source foler (method generateClassSpecFromSourceFolder, javadoc version) 
 */
public class ClassSpecificationGenerator {

	//////////////////////////////////////////////////////////////////////////
	//
	
	/**
	 * Creates class specification for the specified classes which can be found from 
	 * the binary classes in the Jar file at the given jarURL.
	 * 
     * @param jarURL	location of the Jar file to be added to the classpath at runtime in order to find classes
     * @param classes	a list of binary names (e.g., java.lang.String) of the classes whose specification will be generated
     * @param outputFileName	the name of the generated class specification file
     * 
     * @throws ClassNotFoundException	if any class in classes cannot be found
     * @throws MalformedURLException	if the jarURL is malformed
     * @throws IOException	if any error occur when writing out class specification
     * @throws Exception	if the Jar file cannot be added to the classpath at runtime
	 */
	public static void createForClassesFromJar(String jarURL, List classes, String outputFileName)
		throws ClassNotFoundException, MalformedURLException, IOException, Exception
	{
		ClassSpecFromJarGenerator.generate(jarURL, classes, outputFileName);
	}
	
	/**
	 * Creates class specification for all the classes stored in the Jar file at the given jarURL.
	 * 
     * @param jarURL	location of the Jar file to be added to the classpath at runtime in order to find classes
     * @param outputFileName	the name of the generated class specification file
	 * 
     * @throws ClassNotFoundException	if any class cannot be found
     * @throws MalformedURLException	if the jarURL is malformed
     * @throws IOException	if any error occur when writing out class specification
     * @throws Exception	if the Jar file cannot be added to the classpath at runtime
	 */
	public static void createForAllClassesFromJar(String jarURL, String outputFileName)
		throws ClassNotFoundException, MalformedURLException, IOException, Exception
	{
		List classes = getClassNamesFromJar(jarURL);
		ClassSpecFromJarGenerator.generate(jarURL, classes, outputFileName);
	}
	
	private static List getClassNamesFromJar(String jarURL) throws IOException
	{
		String classExt = ".class";
		int extLength = classExt.length();
		List classes = new ArrayList();
		
		JarFile jarFile = new JarFile(jarURL);
		for (Enumeration en = jarFile.entries(); en.hasMoreElements();)
		{
			JarEntry entry = (JarEntry) en.nextElement();
			String name = entry.getName();
			if(name.toLowerCase().endsWith(classExt))
			{
				name = name.replace('/', '.');
				name = name.substring(0, name.length() - extLength);
				classes.add(name);
			}
		}
		
		return classes;
	}
	
	/**
	 * Creates class specification for the specified class which can be found from 
	 * the binary classes in the classpath.
	 * 
	 * @param classBinaryName	binary name (e.g., java.lang.String) of the class whose specification to be generated
	 * @param outputFileName	the name of the generated class specification file
	 * 
	 * @throws ClassNotFoundException	if the specified class cannot be found
	 * @throws IOException	if any error occur when writing out the class specification
	 */
	public static void createForClass(String classBinaryName, String outputFileName)
		throws ClassNotFoundException, IOException
	{
		List classes = new ArrayList();
		classes.add(classBinaryName);
		ClassSpecFromJarGenerator.generate(classes, outputFileName);
	}
	
	/**
	 * Creates class specification for the specified Java source file using Java doclet tool.
	 * 
	 * @param sourceFileName	the Java source file name
	 * @param outputDirectory	the directory where the class specification to be output
	 * @param docfile	output file name, if not specified, output the class specification file to subdirectories in source's package structure
	 * 
	 * @throws ClassNotFoundException	if the Java doclet class cannot be found, usually missing tools.jar
	 * @throws Exception	if any error occur when generating the class specification
	 */
	public static void createForSource(String sourceFileName, String outputDirectory, String docfile)
		throws ClassNotFoundException, Exception
	{
		String[] params = {"-docletpath", ".",
				"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
				"-d", outputDirectory, 
				"-docfile", docfile, 
				sourceFileName};
		
		executeDoclet(params);
	}
	
	/**
	 * Creates class specifications for the specified Java souce files using Java doclet tool.
	 *  
	 * @param sourceFileNames	the Java source file names
	 * @param outputDirectory	the directory where the class specification to be output
	 * @param docfile	output file name, if not specified, output the class specification files to subdirectories in sources' package structure
	 * 
	 * @throws ClassNotFoundException	if the Java doclet class cannot be found, usually missing tools.jar
	 * @throws Exception	if any error occur when generating the class specification
	 */
	public static void createForSources(List sourceFileNames, String outputDirectory, String docfile)
		throws ClassNotFoundException, Exception
	{
		if (!(sourceFileNames == null || sourceFileNames.isEmpty()))
		{
			String[] params = new String[sourceFileNames.size()+8];
			params[0] = "-docletpath";	params[1] = ".";
			params[2] = "-doclet";	params[3] = "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet";
			params[4] = "-d";	params[5] = outputDirectory;
			params[6] = "-docfile";	params[7] = docfile;
			for (int i = 0; i < sourceFileNames.size(); i++)
				params[i+8]= (String) sourceFileNames.get(i);
			
			executeDoclet(params);
		}
	}
	
	/**
	 * Creates class specifications for the Java souce files in the specified folder using Java doclet tool.
	 * 
	 * @param sourceFolderName	the folder that contains Java souce files
	 * @param outputDirectory	the directory where the class specification to be output
	 * @param docfile	output file name, if not specified, output the class specification files to subdirectories in sources' package structure
	 * 
	 * @throws ClassNotFoundException	if the Java doclet class cannot be found, usually missing tools.jar
	 * @throws Exception	if any error occur when generating the class specification
	 */
	public static void createForSourceFolder(String sourceFolderName, String outputDirectory, String docfile)
		throws ClassNotFoundException, Exception
	{
		// get all java file names in the folder
		final File folder = new File(sourceFolderName);
		String[] files = folder.list(new FilenameFilter(){
			public boolean accept(File dir, String name){
				return (dir.equals(folder) && name.toLowerCase().endsWith("java"));
			}
		});
		
		// sourceFolderName is a non-empty folder
		if (!(files == null || files.length == 0))
		{
			// create a list of full file names
			List fullFileNames = new ArrayList();
			for (int i = 0; i < files.length; i ++)
				fullFileNames.add(new File(sourceFolderName, files[i]).getAbsolutePath());
			
			createForSources(fullFileNames, outputDirectory, docfile);
		}
	}

	/**
	 * Creates class specifications for the Java source files with the specified package name.
	 * For example, if the Java souce files are stored at folder "C:/src/com/sun/", the package
	 * name is "com.sun", and source path is "C:/src/".
	 * 
	 * @param packageName	package name of the Java souce files, e.g, com.sun
	 * @param sourcePath	root path of the source files
	 * @param outputDirectory	the directory where the class specification to be output
	 * @param docfile	output file name, if not specified, output the class specification files to subdirectories in sources' package structure
	 * 
	 * @throws ClassNotFoundException	if the Java doclet class cannot be found, usually missing tools.jar
	 * @throws Exception	if any error occur when generating the class specification
	 */
	public static void createForSourcePackage(String packageName, String sourcePath, String outputDirectory, String docfile)
		throws ClassNotFoundException, Exception
	{
		String[] params = {"-docletpath", ".",
				"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
				"-d", outputDirectory, 
				"-docfile", docfile, 
				"-classpath", sourcePath, 
				"subpackages", packageName};
		
		executeDoclet(params);
	}
	
	private static void executeDoclet(String[] params) 
		throws ClassNotFoundException, Exception
	{
		ClassLoader.getSystemClassLoader().loadClass("com.sun.tools.javadoc.Main");
		int state = com.sun.tools.javadoc.Main.execute(params);
		if (state < 0)
			throw new Exception("Javadoc failed to generate class specification, error code: " + state);
	}
	
	//
	//////////////////////////////////////////////////////////////////////////

	public final static String DEFAULT_FILE_LISTING_LOCATION = "temp";
	public final static String DEFAULT_FILE_LISTING_NAME = "SB_INTERNAL_FILELISTING_FILE.txt";

	/**
	 * Constructor
	 * @deprecated by JL
	 */
	public ClassSpecificationGenerator() {
		super();
	}
	
	/**
	 * Generate class specification file from Java source file
	 * (given fullpath filename (javadoc version)
	 * @param inputFile the name of the java file to generate the class spec
  	 * @param outputDirectory The specified directory's path must be surrounded 
	 * by quotation marks(") if it contains any spaces, e.g. "c:/my projects/".
	 * @param docfile output file, if not specified, output specification files to subdirectories in source's package stucture
	 * @return status 0 is successful. -1 if there is an error
	 * @deprecated by JL, replaced by {@link #createForSource(String, String, String)}
	 */
	public static int generateClassSpecFromSource(String inputFile, String outputDirectory, String docfile){
		// e.g. call with
		// outputDirectory c:/docs 
		// docfile docfile.txt
		// inputFile C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/editor/Util.java		
		// see http://java.sun.com/j2se/1.5.0/docs/guide/javadoc/standard-doclet.html#runningprogrammatically
		int status = com.sun.tools.javadoc.Main.execute(new String[] {
					"-docletpath", ".",
					"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
					"-d", outputDirectory, "-docfile", docfile, inputFile});	//TODO: handle possible NoClassDefFoundError
		return status;
	}
	
	/**
	 * Generate class specification file from all Java source file in the given foler (javadoc version)
	 * @param inputFolder folder containing all the java files to generate the class spec
  	 * @param outputDirectory The specified directory's path must be surrounded 
	 * by quotation marks(") if it contains any spaces, e.g. "c:/my projects/".
	 * @param docfile output file, if not specified, output specification files to subdirectories in source's package stucture
	 * @return status 0 is successful. -1 if there is an error
	 * @deprecated by JL, replaced by {@link #createForSourceFolder(String, String, String)}
	 */
	public static int generateClassSpecFromSourceFolder(String inputFolder, String outputDirectory, String docfile){
		
		File folder = new File(inputFolder);
		if(folder == null || !folder.isDirectory()){//TODO: replace numeric error codes - JL
			return -1;
		}
		
		// this does not work	
		//TODO: why doesn't this work? it seems that this code tries to find all java files in the inputFolder
		// without searching sub-folders.
		ArrayList inputFiles = new ArrayList();
		String files[] = folder.list();
		for(int i=0;i<files.length;i++){
			if(files[i]!=null && files[i].endsWith(".java")){//TODO case - JL
				inputFiles.add(inputFolder+"/"+files[i]);
			}
		}
		
		if(inputFiles.isEmpty()){
			return 0; // no files to process
		}
		String param[] = new String[inputFiles.size()+8];
		param[0] = "-docletpath";
		param[1] = ".";
		param[2] = "-doclet";
		param[3] = "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet";
		param[4] = "-d";
		param[5] = outputDirectory;
		param[6] = "-docfile";
		param[7] = docfile;
		for(int i=0;i<inputFiles.size();i++){
			param[i+8]=(String)inputFiles.get(i);
		}
		int status = com.sun.tools.javadoc.Main.execute(param);	//TODO: handle possible NoClassDefFoundError
		return status;
	}
	
	/**
	 * Generate class specification file from Java source package
	 * given java source path and java package name (javadoc version). 
	 * Note that the package can contain other packages
	 * @param packageName class spec will be generated for all the java file under this package
	 * @param sourcePath path of the package
  	 * @param outputDirectory The specified directory's path must be surrounded 
	 * by quotation marks(") if it contains any spaces, e.g. "c:/my projects/".
	 * @param docfile output file, if not specified, output specification files to subdirectories in source's package stucture
	 * @return status 0 is successful. -1 if there is an error
	 * @deprecated by JL, replaced by {@link #createForSourcePackage(String, String, String, String)}
	 */
	public static int generateClassSpecFromPackageSource(String packageName, String sourcePath, String outputDirectory, String docfile){
		// e.g. call with
		// outputDirectory c:/docs 
		// docfile docfile.txt
		// sourcepath = "C:/projects/SimBionic/Code/Java/src/";
		// packageName = "com.stottlerhenke.simbionic.api"; // a package
		//		 see http://java.sun.com/j2se/1.5.0/docs/guide/javadoc/standard-doclet.html#runningprogrammatically	
		int status = com.sun.tools.javadoc.Main.execute(new String[] {
					"-docletpath", ".",
					"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
					"-d", outputDirectory, "-docfile", docfile, "-classpath", sourcePath, "subpackages", packageName});
		return status;
	}
	
	/**
	 * Generate class specification file from Java class file
	 * @param classpath full path filename of the jar file (optional, can be null)
  	 * @param fileListing file listing the classes to be analyzed
	 * @param outputPath path for the generated specification file
	 * @return status null if successful. o.w. error message is returned.
	 * @deprecated by JL, replaced by {@link #createForClassesFromJar(String, List, String)}
	 */
	public static String generateClassSpecFromClass(String classpath, String fileListing, String outputPath){

		String error = null;
		//e.g. call with -cl C:\projects\Warcon2\dev\spec\classListing.txt -o C:\projects\Warcon2\dev\spec\classSpec.txt
		if(classpath==null || "".equals(classpath)){
			// classpath not included
			error =ClassSpecFromJarGenerator.execute(new String[] {
					"-cl", fileListing, 
					"-o", outputPath});
		} else {
			error =ClassSpecFromJarGenerator.execute(new String[] {
				"-jar", classpath, 
				"-cl", fileListing, 
				"-o", outputPath});
		}
	    return error;
	}
	
	/**
	 * Generate class specification file for all the java classes in a jar file
	 * @param inputFile the name of the jar file to generate the class spec
  	 * @param outputPath output file pathname
	 * @return status null if successful. o.w. error message is returned.
	 * @deprecated by JL, replaced by {@link #createForAllClassesFromJar(String, String)}
	 */
	public static String generateClassSpecFromJar(String inputFile, String outputPath){
		// get all the files in the jar, filter those only with .class extension,
		// and replace the '/' in the path to '.' Next, generate a class listing file and use SB version to generate the class spec. 
		Enumeration files =null;
		try {
			JarFile jarFile = new JarFile(inputFile);
			files = jarFile.entries();
		} catch (Exception ex){
			return ex.getMessage();
		}	
	        
		FileWriter fileWriter = null;
	    // write the spec out
		
		// TODO: replace the temporary file which stores the list of class names 
		// 	with just an array of class names. Do not rely on the old code.
		// prepare a file listing file
		String location = System.getProperty("user.dir")+File.separator+DEFAULT_FILE_LISTING_LOCATION
		+ File.separator+ DEFAULT_FILE_LISTING_NAME; //File.separator
		File file = null;
		try{
			file = new File(location);
			fileWriter = new FileWriter(file,false);     
			
		}catch (IOException e)
        {
            return "Error writing class spec file: " + e.getMessage();
        }

		try{
			while(files.hasMoreElements()){
				ZipEntry entry = (ZipEntry)files.nextElement();
				String fileName = entry.getName();
				if(fileName.endsWith(".class")){
					fileName = fileName.replace('/', '.');
					fileName = fileName.substring(0, fileName.length()-6);
					fileWriter.write(fileName+"\n");
				}
			}
			fileWriter.close();
		} catch (IOException e)
        {
            return "Error writing class spec file: " + e.getMessage();
        }
		// class path is the jar file
		String error = generateClassSpecFromClass(inputFile, location, outputPath);
		if(file!=null){
			file.delete(); // clean up the temporary file used
		}
		return error;
	}
	
	
	
	/**
	 * Generate class specification file for the given java class name
	 * @param className java class name
 	 * @param outputPath output file pathname * @return status null if successful. o.w. error message is returned.
 	 * @deprecated by JL, replaced by {@link #createForClass(String, String)}
	 */
	public static String generateClassSpecFromClassname(String className, String outputPath){
     
		FileWriter fileWriter = null;
	    // write the spec out
		
		// prepare a file listing file
		String location = System.getProperty("user.dir")+File.separator+DEFAULT_FILE_LISTING_LOCATION
		+ File.separator+ DEFAULT_FILE_LISTING_NAME; //File.separator
		File file = null;
		try{
			file = new File(location);
			// ensure the folder does exist
			if (!file.getParentFile().exists())
				file.getParentFile().mkdirs();
			fileWriter = new FileWriter(file,false);     
			
		}catch (IOException e)
        {
            return "Error writing class spec file: " + e.getMessage();
        }

		try{
			fileWriter.write(className+"\n");
			fileWriter.close();
		} catch (IOException e)
        {
            return "Error writing class spec file: " + e.getMessage();
        }
		// class path is the jar file
		String error = generateClassSpecFromClass(null, location, outputPath);
		if(file!=null){
			file.delete(); // clean up the temporary file used
		}
		return error;
	}
	

	/**
	 * Test cases
	 * @param arg
	 */
	public static void main(String[] arg) throws Exception{
		
		createForClass("java.lang.Math", "Math.txt");
		
/*		String outputDirectory = "c:/temp";
		String docfile = "classspec.txt";
		
		createForAllClassesFromJar("C:/projects/SimBionic/Code/Java/lib/MultiEditor.jar", "c:/temp/spec1.txt");
		
		createForClass("java.lang.Math", "c:/temp/spec2.txt");
		
		List classes = new ArrayList();
		classes.add("com.shai.flexitrainer.multieditor.AbstractEditor");
		createForClassesFromJar("C:/projects/SimBionic/Code/Java/lib/MultiEditor.jar", classes, "c:/temp/spec3.txt");
		
		createForSource("C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api/SB_Engine.java", outputDirectory, "spec4.txt");
		
		createForSourceFolder("C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api/", outputDirectory, "spec5.txt");
		
		createForSourcePackage("com.stottlerhenke.simbionic.api", "C:/projects/SimBionic/Code/Java/src/", outputDirectory, "spec6.txt");
		
// this is supported
		String inputFile = "C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api/SB_Engine.java";
		// this is not supported
		//String inputFile = "C:/projects/SimBionic/Code/Java/bin/com/stottlerhenke/simbionic/api/SB_Engine.class";
		// this is not supported
		//String inputFile = "C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api/SB_*.java";	
		/*
		int status = com.sun.tools.javadoc.Main.execute(new String[] {
				"-docletpath", ".",
				"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
				"-d", outputDirectory, "-docfile", docfile, inputFile});
*/

//		String sourcepath = "C:/projects/SimBionic/Code/Java/src/";
// this is supported
//		inputFile = "com.stottlerhenke.simbionic.api"; // a package
		// this is not supported
		//inputFile = "com.stottlerhenke.simbionic.api.SB_Engine";
		// inputFile = "com.stottlerhenke.simbionic"; // need to use "-subpackages"	
		// this is not supported
		//sourcepath = "C:/projects/SimBionic/Code/Java/b/";
		//inputFile = "com.stottlerhenke.simbionic.api.SB_Engine";
	/*	int status = com.sun.tools.javadoc.Main.execute(new String[] {
				"-docletpath", ".",
				"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
				"-d", outputDirectory, "-docfile", docfile, "-classpath", sourcepath, "-subpackages", inputFile});
		*/
		// both "-sourcepath", and "-classpath" work
				
		// if generate from jar (or class files), should call the other class spec generator instead.
		// test jar, not work
		/*String sourcepath = "C:/projects/SimBionic/Code/Java/src/multieditor-2.0.jar";
		inputFile = "com.shai.flexitrainer.common.FlexiTrainerUtil";
		int status = com.sun.tools.javadoc.Main.execute(new String[] {
				"-docletpath", ".",
				"-doclet", "com.stottlerhenke.simbionic.util.ClassSpecificationDoclet",
				"-d", outputDirectory, "-docfile", docfile, "-classpath", sourcepath, inputFile});
				*/
	//	System.out.println("generate class spec from jar");
		//sourcepath = "C:/projects/SimBionic/Code/Java/multieditor-2.0.jar";
		// this does not work
	//	sourcepath = "C:/projects/SimBionic/Code/Java/xml-apis.jar";
	//	sourcepath = "C:/projects/SimBionic/Code/Java/grist.jar";
		// this works
		//sourcepath = "C:/projects/SimBionic/Code/Java/jsapi.jar";
		//Runtime.getRuntime().loadLibrary(sourcepath);
		//generateClassSpecFromJar(sourcepath, outputDirectory+docfile);	    	
	        // Create the jar class loader and use the jar file to use. 
		//inputFile = "C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api";
		//generateClassSpecFromSourceFolder( inputFile, outputDirectory, docfile);
		//inputFile = "C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api/SB_Config.java";
//		inputFile = "C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api";
		//String inputFile2 = "C:/projects/SimBionic/Code/Java/src/com/stottlerhenke/simbionic/api/SB_Config.java";
		
//		generateClassSpecFromSourceFolder( inputFile, outputDirectory, docfile);
		
		//String fileListing = "c:/classlisting.txt";
	///	String classpath = "C:\\projects\\SimBionic\\Code\\Java\\bin\\com\\stottlerhenke\\simbionic\\editor\\gui";
		// generateClassSpecFromClass(classpath, fileListing,  outputDirectory+docfile);

			 
	}
}
